package com.google.code.gronono.gps.ui.controller;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.text.MessageFormat;

import javax.swing.Action;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.JTextField;

import org.apache.log4j.Logger;

import com.google.code.gronono.commons.io.file.FileUtils;
import com.google.code.gronono.commons.swing.dialog.ProgressDialog;
import com.google.code.gronono.gps.model.Configuration;
import com.google.code.gronono.gps.model.GPSService;
import com.google.code.gronono.gps.model.SortedFile;
import com.google.code.gronono.gps.model.enums.ProcessAction;
import com.google.code.gronono.gps.ui.GPSFrame;
import com.google.code.gronono.gps.ui.controller.actions.BrowseAction;
import com.google.code.gronono.gps.ui.controller.actions.OptionsAction;
import com.google.code.gronono.gps.ui.controller.actions.PreviewAction;
import com.google.code.gronono.gps.ui.controller.actions.QuitAction;
import com.google.code.gronono.gps.ui.controller.actions.ResetAction;
import com.google.code.gronono.gps.ui.controller.actions.SortAction;

import fr.gronono.gbundle.Bundle;

/**
 * Contrôleur de l'application GPS.
 */
@Bundle("com.google.code.gronono.gps.gui")
public class GPSController {
	/** Logger. */
	private static final Logger logger = Logger.getLogger(GPSController.class);

	/** Titre de la fenêtre de confirmation de sortie. */
	@Bundle("controller.exit.confirm.title")
	private static String EXIT_TITLE;
	/** Message de la fenêtre de confirmation de sortie. */
	@Bundle("controller.exit.confirm.message")
	private static String EXIT_MSG;

	/** Option YES de la fenêtre de confirmation de sortie. */
	@Bundle("controller.exit.confirm.yes.option")
	private static String EXIT_YES_OPTION;
	/** Option NO de la fenêtre de confirmation de sortie. */
	@Bundle("controller.exit.confirm.no.option")
	private static String EXIT_NO_OPTION;

	/** Largeur de la barre de progression. */
	@Bundle("controller.progress.dialog.width")
	private static int PROGRESS_WIDTH;
	/** Hauteur de la barre de progression. */
	@Bundle("controller.progress.dialog.height")
	private static int PROGRESS_HEIGHT;
	/** Titre de la barre de progression. */
	@Bundle("controller.progress.dialog.title")
	private static String PROGRESS_TITLE;
	/** Description de la barre de progression. */
	@Bundle("controller.progress.dialog.description")
	private static String PROGRESS_DESCR;

	/** Action de réinitialisation des options. */
	private Action resetAction;
	/** Action d'affichage de la page d'options. */
	private Action optionsAction;
	/** Action d'aperçu. */
	private Action previewAction;
	/** Action de sortie de l'application. */
	private Action quitAction;
	/** Action de tri. */
	private Action sortAction;
	/** Action de sélection d'un répertoire. */
	private Action browseAction;

	/** Service de l'application. */
	private final GPSService gpsService;
	/** Configuration de l'application. */
	private Configuration configuration;
	/** Frame principale de l'application. */
	private final GPSFrame gpsFrame;

	/** Flag indiquant si la configuration nécessite une sauvegarde. */
	private boolean needConfSaved = false;

	/**
	 * Constructeur.
	 * @param configuration La configuration.
	 */
	public GPSController(final Configuration configuration) {
		gpsService = new GPSService(configuration);

		this.configuration = configuration;
		// Listener sur les propriétés de la configuration
		configuration.addPropertyChangeListener(new PropertyChangeListener() {
			public void propertyChange(final PropertyChangeEvent evt) {
				needConfSaved = true;
			}
		});

		createActions();

		gpsFrame = new GPSFrame(this);

		// Affiche la page d'options
		this.options();

		// Initialisation du flag de sauvegarde
		needConfSaved = false;
	}

	/** Initialisation des actions. */
	private void createActions() {
		resetAction = new ResetAction(this);
		optionsAction = new OptionsAction(this);
		previewAction = new PreviewAction(this);
		quitAction = new QuitAction(this);
		sortAction = new SortAction(this);
		browseAction = new BrowseAction(this);
	}

	/**
	 * Retourne la frame principale de l'application.
	 * @return GPSFrame La frame principale de l'application.
	 */
	public GPSFrame getGPSFrame() {
		return gpsFrame;
	}

	/** Quitte l'application. */
	public void quit() {
		if (logger.isTraceEnabled()) logger.trace("quit");

		boolean exit = true;

		// Si la configuration a changé
		if (needConfSaved) {
			// Poser la question de la sauvegarde de la configuration
			final int r = JOptionPane.showOptionDialog(gpsFrame, new Object[] { EXIT_MSG }, EXIT_TITLE, JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, new Object[] { EXIT_YES_OPTION, EXIT_NO_OPTION }, EXIT_YES_OPTION);
			// Traitement du cas de fermeture de la popup (croix, échap)
			if (r != JOptionPane.CLOSED_OPTION) {
				// Annulation du traitement en cours (au cas où)
				gpsService.cancel();

				// Si ok, sauvegarde de la configuration
				if (r == JOptionPane.YES_OPTION) {
					try {
						this.configuration.save();
					} catch (final IOException ex) {
						// Pas d'affichage car a priori ne devrait pas arriver (sinon serait déjà arrivé au lancement de l'appli...)
						logger.error("IOException : " + ex.getMessage() + (ex.getCause() != null ? ", " + ex.getCause() : ""), ex);
					}
				}
			} else {
				// Retour à l'application
				exit = false;
			}
		} else {
			// Sinon on quitte directement
		}

		if (exit) {
			// Libération du composant graphique principal
			gpsFrame.dispose();

			logger.info(EXIT_TITLE);
		}
	}

	/** Effectue le traitement en mode aperçu et affiche la page de résultats. */
	public void preview() {
		// Effectue le traitement en mode preview
		final boolean result = process(true);
		// Affiche le résultat si le traitement n'a pas été annulé
		if (result) {
			gpsFrame.displayPreview();
			resetAction.setEnabled(true);
			optionsAction.setEnabled(true);
			previewAction.setEnabled(false);
			sortAction.setEnabled(true);
			resetAction.setEnabled(false);
		}
	}

	/** Effectue physiquement le traitement de tri et affiche la page de résultats. */
	public void sort() {
		// Effectue physiquement le traitement de tri
		final boolean result = process(false);
		// Affiche le résultat si le traitement n'a pas été annulé
		if (result) {
			gpsFrame.displaySort();
			resetAction.setEnabled(true);
			optionsAction.setEnabled(true);
			previewAction.setEnabled(false);
			sortAction.setEnabled(false);
			resetAction.setEnabled(false);
		}
	}

	/**
	 * Effectue le traitement.
	 * @param preview Mode aperçu
	 * @return <code>true</code> Si le traitement n'a pas été annulé, <code>false</code> sinon.
	 */
	private boolean process(final boolean preview) {
		// Gestion de la réinitialisation des résultats
		this.gpsService.reset();

		// Affichage d'une barre de progression
		final ProgressDialog progressDialog = new ProgressDialog(gpsFrame, PROGRESS_TITLE);
		progressDialog.setSize(PROGRESS_WIDTH, PROGRESS_HEIGHT);
		progressDialog.start(new Runnable() {
			public void run() {
				PropertyChangeListener currentFileListener = null;
				try {
					// Ajout d'un listener sur le fichier courant (pour affichage dans la progressbar)
					currentFileListener = new PropertyChangeListener() {
						public void propertyChange(final PropertyChangeEvent evt) {
							final File currentFile = (File) evt.getNewValue();
							final String srcDir = configuration.getSrcDir().getAbsolutePath();
							final String parent = currentFile.getParent();
							if (parent == null) {
								progressDialog.setDescription(MessageFormat.format(PROGRESS_DESCR, srcDir));
							} else {
								if ((parent.length() > srcDir.length()) && parent.startsWith(srcDir)) {
									final String dir = parent.substring(srcDir.length() + 1);
									progressDialog.setDescription(MessageFormat.format(PROGRESS_DESCR, dir));
								}
							}
							progressDialog.setStep(currentFile.getName());
						}
					};
					gpsService.addPropertyChangeListener(GPSService.CURRENT_FILE_PROPERTY, currentFileListener);

					// Appel du service effectuant le traitement
					gpsService.process(preview);
				} catch (final Throwable ex) {
					progressDialog.setCancelled();

					final StringBuilder msg = new StringBuilder(ex.getClass().getName()).append(" : ").append(ex.getMessage());
					if (ex.getCause() != null) msg.append(ex.getCause());
					logger.error(msg.toString(), ex);
					JOptionPane.showMessageDialog(null, msg.toString(), ex.getClass().getName(), JOptionPane.ERROR_MESSAGE);
				} finally {
					gpsService.removePropertyChangeListener(currentFileListener);
				}
			}
		});

		// Gestion de l'annulation de l'opération
		if (progressDialog.isCancelled()) {
			gpsService.cancel();
			return false;
		}

		return true;
	}

	/** Affichage de la page d'options. */
	public void options() {
		gpsFrame.displayOptions();
		resetAction.setEnabled(true);
		optionsAction.setEnabled(false);
		previewAction.setEnabled(true);
		sortAction.setEnabled(true);
		resetAction.setEnabled(true);
	}

	/** Réinitialisation de la configuration. */
	public void reset() {
		// Réinitialisation de la configuration
		try {
			// On recharge la configuration par défaut
			configuration = Configuration.load(true);
		} catch (final IOException ex) {
			// Pas d'affichage car a priori ne devrait pas arriver (sinon serait déjà arrivé au lancement de l'appli...)
			logger.error("IOException : " + ex.getMessage() + (ex.getCause() != null ? ", " + ex.getCause() : ""), ex);
		}
		gpsFrame.resetOptions();
		gpsFrame.displayOptions();
		resetAction.setEnabled(true);
		optionsAction.setEnabled(false);
		previewAction.setEnabled(true);
		sortAction.setEnabled(true);
	}

	/**
	 * Sélection d'un répertoire.
	 * @param button Le bouton de sélection d'un répertoire.
	 */
	public void browse(final JButton button) {
		// On récupère le textField associé au bouton spécifié (via la clientProperty)
		final JTextField field = (JTextField) button.getClientProperty(BrowseAction.FIELD_KEY);
		// On initialise le fichier sélectionné
		File dir = new File(field.getText());
		// Validation du répertoire
		try {
			FileUtils.checkCanReadDir(dir);
			if (configuration.getProcessAction() == ProcessAction.MOVE) {
				FileUtils.checkCanWriteDir(dir, false);
			}
		} catch (final Exception e) {
			// Repositionne le curseur sur le prochain dossier de plus haut niveau existant
			final File highestExistingDir = FileUtils.getHighestExistingParentFolder(dir);
			if (highestExistingDir != null) {
				dir = highestExistingDir;
				field.setText(highestExistingDir.getAbsolutePath());
			}
		}
		// Sélection des répertoires uniquement
		final JFileChooser fileChooser = new JFileChooser();
		fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
		fileChooser.setCurrentDirectory(dir);
		// Ouverture du dialog et récupération du résultat
		final int result = fileChooser.showOpenDialog(field);
		if (result == JFileChooser.APPROVE_OPTION) {
			field.setText(fileChooser.getSelectedFile().getAbsolutePath());
		}
	}

	/**
	 * Retourne l'action de réinitialisation de la configuration.
	 * @return L'action de réinitialisation de la configuration.
	 */
	public Action getResetAction() {
		return resetAction;
	}

	/**
	 * Retourne l'action d'affichage des options.
	 * @return L'action d'affichage des options.
	 */
	public Action getOptionsAction() {
		return optionsAction;
	}

	/**
	 * Retourne l'action d'affichage de l'aperçu.
	 * @return L'action d'affichage de l'aperçu.
	 */
	public Action getPreviewAction() {
		return previewAction;
	}

	/**
	 * Retourne l'action
	 * @return L'action
	 */
	public Action getQuitAction() {
		return quitAction;
	}

	/**
	 * Retourne l'action de tri.
	 * @return L'action de tri.
	 */
	public Action getSortAction() {
		return sortAction;
	}

	/**
	 * Retourne l'action de sélection d'un fichier.
	 * @return Action L'action de sélection.
	 */
	public Action getBrowseAction() {
		return browseAction;
	}

	/**
	 * Retourne la configuration.
	 * @return La configuration.
	 */
	public Configuration getConfiguration() {
		return configuration;
	}

	/**
	 * Retourne le résultat de traitement.
	 * @return Le tableau de résultats.
	 */
	public SortedFile[] getSortedFiles() {
		return gpsService.getProcessFiles();
	}
}
